package com.dayang.redbook.user.biz.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.dayang.framework.biz.context.holder.LoginUserContextHolder;
import com.dayang.framework.common.domain.response.Response;
import com.dayang.framework.common.exception.BizException;
import com.dayang.framework.common.util.JsonUtil;
import com.dayang.framework.common.util.ParamUtils;
import com.dayang.redbook.user.biz.constant.RedisKeyConstants;
import com.dayang.redbook.user.biz.constant.RoleConstants;
import com.dayang.redbook.user.biz.domain.dataobject.RoleDO;
import com.dayang.redbook.user.biz.domain.dataobject.UserDO;
import com.dayang.redbook.user.biz.domain.dataobject.UserRoleDO;
import com.dayang.redbook.user.biz.domain.mapper.RoleDOMapper;
import com.dayang.redbook.user.biz.domain.mapper.UserDOMapper;
import com.dayang.redbook.user.biz.domain.mapper.UserRoleDOMapper;
import com.dayang.redbook.user.biz.enums.DeletedEnum;
import com.dayang.redbook.user.biz.enums.ResponseCodeEnum;
import com.dayang.redbook.user.biz.enums.SexEnum;
import com.dayang.redbook.user.biz.enums.StatusEnum;
import com.dayang.redbook.user.biz.model.vo.UpdateUserInfoReqVO;
import com.dayang.redbook.user.biz.rpc.DistributedIdGeneratorRpcService;
import com.dayang.redbook.user.biz.rpc.OssRpcService;
import com.dayang.redbook.user.biz.service.UserService;
import com.dayang.redbook.user.dto.req.*;
import com.dayang.redbook.user.dto.resp.FindUserByIdRspDTO;
import com.dayang.redbook.user.dto.resp.FindUserByPhoneRspDTO;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author: dayang
 * @since: 2024/7/4
 * @description:
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserDOMapper userDOMapper;
    private final OssRpcService ossRpcService;
    private final UserRoleDOMapper userRoleDOMapper;
    private final RoleDOMapper roleDOMapper;
    private final RedisTemplate<String, Object> redisTemplate;
    private final DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
    private final ThreadPoolTaskExecutor taskExecutor;

    /**
     * 用户信息本地缓存
     */
    private static final Cache<Long,FindUserByIdRspDTO> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000)
            .maximumSize(10000)
            .expireAfterWrite(1,TimeUnit.HOURS)
            .build();

    @Override
    public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
        UserDO userDO = new UserDO();
        userDO.setId(LoginUserContextHolder.getUserId());
        // 标识位：是否需要更新
        boolean needUpdate = false;

        MultipartFile avatarFile = updateUserInfoReqVO.getAvatar();
        if (Objects.nonNull(avatarFile)){
            long startTime = System.currentTimeMillis();
            String avatar = ossRpcService.uploadFile(avatarFile);
            long endTime = System.currentTimeMillis();
            log.info("==> 调用 oss 服务成功，上传头像，url：{} ，上传时间：{}", avatar,endTime-startTime);
            // 若上传头像失败，则抛出业务异常
            if (StrUtil.isBlank(avatar)) {
                throw new BizException(ResponseCodeEnum.UPLOAD_AVATAR_FAIL);
            }
            userDO.setAvatar(avatar);
            needUpdate = true;
        }

        // 昵称
        String nickname = updateUserInfoReqVO.getNickname();
        if (StrUtil.isNotBlank(nickname)) {
            Preconditions.checkArgument(ParamUtils.checkNickname(nickname), ResponseCodeEnum.NICK_NAME_VALID_FAIL);
            userDO.setNickname(nickname);
            needUpdate = true;
        }
        // 小哈书号
        String redbookId = updateUserInfoReqVO.getRedbookId();
        if (StrUtil.isNotBlank(redbookId)) {
            Preconditions.checkArgument(ParamUtils.checkRedbookId(redbookId), ResponseCodeEnum.REDBOOK_ID_VALID_FAIL);
            userDO.setRedbookId(redbookId);
            needUpdate = true;
        }

        // 性别
        Integer sex = updateUserInfoReqVO.getSex();
        if (Objects.nonNull(sex)) {
            Preconditions.checkArgument(SexEnum.isValid(sex), ResponseCodeEnum.SEX_VALID_FAIL);
            userDO.setSex(sex);
            needUpdate = true;
        }

        // 生日
        LocalDate birthday = updateUserInfoReqVO.getBirthday();
        if (Objects.nonNull(birthday)) {
            userDO.setBirthday(birthday);
            needUpdate = true;
        }

        // 个人简介
        String introduction = updateUserInfoReqVO.getIntroduction();
        if (StrUtil.isNotBlank(introduction)) {
            Preconditions.checkArgument(ParamUtils.checkLength(introduction, 100), ResponseCodeEnum.INTRODUCTION_VALID_FAIL);
            userDO.setIntroduction(introduction);
            needUpdate = true;
        }

        // 背景图
        MultipartFile backgroundImgFile = updateUserInfoReqVO.getBackgroundImg();
        if (Objects.nonNull(backgroundImgFile)) {
            // todo: 调用对象存储服务上传文件
        }

        if (needUpdate) {
            // 更新用户信息
            userDO.setUpdateTime(LocalDateTime.now());
            userDOMapper.updateByPrimaryKeySelective(userDO);
        }
        return Response.success();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<Long> register(RegisterUserReqDTO registerUserReqDTO) {
        String phone = registerUserReqDTO.getPhone();
        // 先判断该手机号是否已被注册
        UserDO userDO1 = userDOMapper.selectByPhone(phone);
        log.info("==> 用户是否注册, phone: {}, userDO: {}", phone, JsonUtil.toJsonString(userDO1));
        // 若已注册，则直接返回用户ID
        if (Objects.nonNull(userDO1)){
            return Response.success(userDO1.getId());
        }
        // 否则注册新用户
        // 获取全局自增的小红书 ID
        // Long redBookId = redisTemplate.opsForValue().increment(RedisConstants.REDBOOK_ID_GENERATOR_KEY);
        String userIdStr = distributedIdGeneratorRpcService.getUserId();
        String redBookId = distributedIdGeneratorRpcService.getRedBookId();
        UserDO userDO = UserDO.builder()
                .id(Long.valueOf(userIdStr))
                .phone(phone)
                .redbookId(redBookId) // 自动生成小红书号 ID
                .nickname("小红薯" + redBookId) // 自动生成昵称, 如：小红薯10000
                .status(StatusEnum.ENABLE.getValue()) // 状态为启用
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .isDeleted(DeletedEnum.NO.getValue()) // 逻辑删除
                .build();
        userDOMapper.insert(userDO);

        Long userId = userDO.getId();

        // 给该用户分配一个默认角色
        UserRoleDO userRoleDO = UserRoleDO.builder()
                .userId(userId)
                .roleId(RoleConstants.COMMON_USER_ROLE_ID)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .isDeleted(DeletedEnum.NO.getValue())
                .build();
        userRoleDOMapper.insert(userRoleDO);
        RoleDO roleDO = roleDOMapper.selectByPrimaryKey(RoleConstants.COMMON_USER_ROLE_ID);
        // 将该用户的角色 ID 存入 Redis 中
        List<String> roles = new ArrayList<>(1);
        roles.add(roleDO.getRoleKey());
        String userRolesKey = RedisKeyConstants.buildUserRoleKey(userId);
        redisTemplate.opsForValue().set(userRolesKey, JsonUtil.toJsonString(roles));
        return Response.success(userId);
    }

    /**
     * 根据手机号查询用户信息
     * @param findUserByPhoneReqDTO
     * @return
     */
    @Override
    public Response<FindUserByPhoneRspDTO> findByPhone(FindUserByPhoneReqDTO findUserByPhoneReqDTO) {
        String phone = findUserByPhoneReqDTO.getPhone();
        UserDO userDO = userDOMapper.selectByPhone(phone);
        if (Objects.isNull(userDO)) {
            throw new BizException(ResponseCodeEnum.USER_NOT_FOUND);
        }
        FindUserByPhoneRspDTO findUserByPhoneRspDTO = FindUserByPhoneRspDTO.builder()
                .id(userDO.getId())
                .password(userDO.getPassword())
                .build();
        return Response.success(findUserByPhoneRspDTO);
    }

    /**
     * 更新密码
     * @param updateUserPasswordReqDTO
     * @return
     */
    @Override
    public Response<?> updatePassword(UpdateUserPasswordReqDTO updateUserPasswordReqDTO) {
        // 获取当前请求对应的用户 ID
        Long userId = LoginUserContextHolder.getUserId();
        UserDO userDO = UserDO.builder()
                .id(userId)
                .password(updateUserPasswordReqDTO.getEncodePassword()) // 加密后的密码
                .updateTime(LocalDateTime.now())
                .build();
        // 更新密码
        userDOMapper.updateByPrimaryKeySelective(userDO);
        return Response.success();
    }

    /**
     * 根据用户 ID 查询用户信息
     * @param findUserByIdReqDTO
     * @return 用户信息
     */
    @Override
    public Response<FindUserByIdRspDTO> findById(FindUserByIdReqDTO findUserByIdReqDTO) {
        Long userId = findUserByIdReqDTO.getId();

        // 先从本地缓存中查询
        FindUserByIdRspDTO findUserByIdRspDTOLocalCache  = LOCAL_CACHE.getIfPresent(userId);
        if (Objects.nonNull(findUserByIdRspDTOLocalCache)){
            // log.info("==> 命中了本地缓存；{}", findUserByIdRspDTOLocalCache);
            return Response.success(findUserByIdRspDTOLocalCache);
        }

        // 构建用户信息缓存key
        String userInfoKey = RedisKeyConstants.buildUserInfoKey(userId);
        // 查询缓存
        String userInfoRedisValue = (String) redisTemplate.opsForValue().get(userInfoKey);
        if (StrUtil.isNotBlank(userInfoRedisValue)){ // 缓存存在 直接返回
            FindUserByIdRspDTO findUserByIdRspDTO = JsonUtil.parseObject(userInfoRedisValue, FindUserByIdRspDTO.class);
            // 异步线程中将用户信息存入本地缓存
            taskExecutor.submit(() -> {
                if (Objects.nonNull(findUserByIdRspDTO)){
                    // 写入本地缓存
                    LOCAL_CACHE.put(userId, findUserByIdRspDTO);
                }

            });
            return Response.success(findUserByIdRspDTO);
        }

        // 缓存不存在，则查询数据库，再一并写入缓存
        UserDO userDO = userDOMapper.selectByPrimaryKey(userId);
        // 判空
        if (Objects.isNull(userDO)) {
            taskExecutor.execute(() -> {
                // 防止缓存穿透，将空数据存入 Redis 缓存 (过期时间不宜设置过长)
                // 保底1分钟 + 随机秒数
                long expireSeconds = 60 + RandomUtil.randomInt(60);
                redisTemplate.opsForValue().set(userInfoKey, "null", expireSeconds, TimeUnit.SECONDS);
            });
            throw new BizException(ResponseCodeEnum.USER_NOT_FOUND);
        }

        FindUserByIdRspDTO findUserByIdRspDTO = FindUserByIdRspDTO.builder()
                .id(userDO.getId())
                .nickName(userDO.getNickname())
                .avatar(userDO.getAvatar())
                .introduction(userDO.getIntroduction())
                .build();
        // 异步将用户信息存入 Redis 缓存，提升响应速度
        taskExecutor.submit(() -> {
            // 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
            long expireSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);
            redisTemplate.opsForValue()
                    .set(userInfoKey, JsonUtil.toJsonString(findUserByIdRspDTO), expireSeconds, TimeUnit.SECONDS);
        });
        return Response.success(findUserByIdRspDTO);
    }

    /**
     * 批量根据用户 ID 查询用户信息
     * @param findUsersByIdsReqDTO
     * @return
     */
    @Override
    public Response<List<FindUserByIdRspDTO>> findByIds(FindUsersByIdsReqDTO findUsersByIdsReqDTO) {
        // 需要查询的用户集合
        List<Long> userIds = findUsersByIdsReqDTO.getIds();

        // 构建rediskey集合
        List<String> redisKeys = userIds.stream().map(RedisKeyConstants::buildUserInfoKey).toList();
        List<Object> redisValues = redisTemplate.opsForValue().multiGet(redisKeys);
        if (CollUtil.isNotEmpty(redisValues)){
            // 过滤掉为空的数据(为了防止缓存穿透,缓存一些为null的数据)
            redisValues = redisValues.stream().filter(Objects::nonNull).toList();
        }
        List<FindUserByIdRspDTO> findUserByIdRspDTOS = Lists.newArrayList();
        if (CollUtil.isNotEmpty(redisValues)) {
            findUserByIdRspDTOS = redisValues.stream()
                    .map(value -> JsonUtil.parseObject(String.valueOf(value), FindUserByIdRspDTO.class))
                    .toList();
        }
        // 如果被查询的用户信息，都在 Redis 缓存中, 则直接返回
        if (CollUtil.size(userIds) == CollUtil.size(findUserByIdRspDTOS)){
            return Response.success(findUserByIdRspDTOS);
        }

        // 还有另外两种情况：一种是缓存里没有用户信息数据，还有一种是缓存里数据不全，需要从数据库中补充
        // 筛选出缓存里没有的用户数据，去查数据库
        List<Long> userIdsNeedQuery;
        if (CollUtil.isNotEmpty(findUserByIdRspDTOS)){
            Map<Long, FindUserByIdRspDTO> map = findUserByIdRspDTOS.stream()
                    .collect(Collectors.toMap(FindUserByIdRspDTO::getId, p -> p));
            userIdsNeedQuery = userIds.stream().filter(id -> Objects.isNull(map.get(id))).toList();

        }else { // 缓存中一条用户信息都没查到,则提交的用户ID集合都需要查数据库
            userIdsNeedQuery = userIds;
        }

        // 从数据库中批量查询
        List<UserDO> userDOS = userDOMapper.selectByIds(userIdsNeedQuery);
        List<FindUserByIdRspDTO> findUserByIdRspDTOS2 = null;
        if (CollUtil.isNotEmpty(userDOS)){
            findUserByIdRspDTOS2  = userDOS.stream()
                    .map(userDO -> FindUserByIdRspDTO.builder()
                            .id(userDO.getId())
                            .nickName(userDO.getNickname())
                            .avatar(userDO.getAvatar())
                            .introduction(userDO.getIntroduction())
                            .build())
                    .toList();
            // 异步线程将用户信息同步到 Redis 中
            List<FindUserByIdRspDTO> finalFindUserByIdRspDTOS = findUserByIdRspDTOS2;
            taskExecutor.submit(() -> {
                Map<Long, FindUserByIdRspDTO> map = finalFindUserByIdRspDTOS.stream().collect(Collectors.toMap(FindUserByIdRspDTO::getId, p -> p));
                redisTemplate.executePipelined((RedisCallback<Void>) connection -> {
                    for (FindUserByIdRspDTO finalFindUserByIdRspDTO : finalFindUserByIdRspDTOS) {
                        Long userId = finalFindUserByIdRspDTO.getId();
                        // 用户信息缓存 Redis Key
                        String userInfoRedisKey = RedisKeyConstants.buildUserInfoKey(userId);
                        // DTO 转 JSON 字符串
                        String value = JsonUtil.toJsonString(finalFindUserByIdRspDTO);
                        // 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
                        long expireSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);
                        redisTemplate.opsForValue().set(userInfoRedisKey, value, expireSeconds, TimeUnit.SECONDS);
                    }
                    return null;
                });
            });
        }
        // 把数据库里面的数据和redis的数据进行合并
        if (CollUtil.isNotEmpty(findUserByIdRspDTOS2)){
            findUserByIdRspDTOS.addAll(findUserByIdRspDTOS2);
        }
        return Response.success(findUserByIdRspDTOS);
    }

}
